import tkinter as tk from tkinter import ttk, messagebox, filedialog, simpledialog import time import threading from pynput import mouse, keyboard import json # --- Configuration --- HOTKEY_RECORD_TOGGLE = keyboard.Key.f8 HOTKEY_PLAY = keyboard.Key.f12 HOTKEY_STOP_PLAYBACK = keyboard.Key.f10 # Pour une reconstruction plus sûre des boutons de souris MOUSE_BUTTON_MAP = { 'left': mouse.Button.left, 'right': mouse.Button.right, 'middle': mouse.Button.middle, 'x1': mouse.Button.x1, 'x2': mouse.Button.x2, } class AutoTask: def __init__(self, root): self.root = root self.root.title("AutoTask") self.root.geometry("350x260") # Légèrement plus grand pour la nouvelle option self.root.resizable(False, False) self.recorded_actions = [] self.is_recording = False self.is_playing = False self.stop_playback_flag = False self.mouse_listener = None self.keyboard_listener = None self.last_event_time = None self.playback_thread = None self.hotkey_listener_thread = None # --- Style (Dark Mode) --- style = ttk.Style() try: style.theme_use('clam') # Base pour la personnalisation except tk.TclError: print("Le thème 'clam' n'est pas disponible, utilisation du thème par défaut.") # Couleurs BG_COLOR = "#2E2E2E" FG_COLOR = "#FFFFFF" BUTTON_BG = "#3C3C3C" BUTTON_FG = "#FFFFFF" ENTRY_BG = "#3C3C3C" ENTRY_FG = "#FFFFFF" DISABLED_FG = "#888888" # Couleur pour le texte des widgets désactivés self.root.configure(bg=BG_COLOR) style.configure('.', background=BG_COLOR, foreground=FG_COLOR, font=('Helvetica', 10)) style.map('.', foreground=[('disabled', DISABLED_FG)], background=[('disabled', BG_COLOR)]) # Couleur de fond pour widgets désactivés style.configure("TButton", padding=6, relief="flat", background=BUTTON_BG, foreground=BUTTON_FG, font=('Helvetica', 10, 'bold')) style.map("TButton", background=[('active', '#555555'), ('disabled', '#303030')], foreground=[('disabled', DISABLED_FG)]) style.configure("TLabel", padding=6, background=BG_COLOR, foreground=FG_COLOR, font=('Helvetica', 10)) style.configure("TEntry", padding=6, font=('Helvetica', 10), fieldbackground=ENTRY_BG, foreground=ENTRY_FG, insertcolor=FG_COLOR) # Couleur du curseur style.map("TEntry", fieldbackground=[('disabled', '#252525')], foreground=[('disabled', DISABLED_FG)]) style.configure("TCheckbutton", padding=6, background=BG_COLOR, foreground=FG_COLOR, font=('Helvetica', 10)) style.map("TCheckbutton", indicatorcolor=[('selected', BUTTON_BG), ('!selected', BUTTON_BG)], foreground=[('disabled', DISABLED_FG)]) style.configure("TMenubutton", background=BUTTON_BG, foreground=BUTTON_FG, font=('Helvetica', 10)) # Configuration du menu (peut être dépendant du système d'exploitation) style.configure("Menu", background=BG_COLOR, foreground=FG_COLOR, relief=tk.FLAT) # --- UI Elements --- self.status_label = ttk.Label(root, text="Prêt. (F8: Rec/Stop, F12: Play, F10: Stop Play)", anchor="center") self.status_label.pack(pady=10, fill=tk.X, padx=10) button_frame = ttk.Frame(root, style="TFrame") # S'assurer que TFrame utilise le style button_frame.pack(pady=10) self.record_button = ttk.Button(button_frame, text="🔴 Enregistrer (F8)", command=self.toggle_record) self.record_button.pack(side=tk.LEFT, padx=5) self.play_button = ttk.Button(button_frame, text="▶️ Lire (F12)", command=self.start_playback) self.play_button.pack(side=tk.LEFT, padx=5) loop_outer_frame = ttk.Frame(root) # Frame pour contenir les options de boucle loop_outer_frame.pack(pady=5, padx=10, fill=tk.X) loop_entry_frame = ttk.Frame(loop_outer_frame) loop_entry_frame.pack(side=tk.LEFT, expand=True, fill=tk.X, anchor=tk.W) ttk.Label(loop_entry_frame, text="Répétitions:").pack(side=tk.LEFT, padx=(0,5)) self.loop_var = tk.StringVar(value="1") self.loop_entry = ttk.Entry(loop_entry_frame, textvariable=self.loop_var, width=5) self.loop_entry.pack(side=tk.LEFT) self.loop_indefinitely_var = tk.BooleanVar(value=False) self.loop_indefinitely_checkbox = ttk.Checkbutton( loop_outer_frame, text="🔄 Boucle Infinie", variable=self.loop_indefinitely_var, command=self.toggle_loop_entry_state ) self.loop_indefinitely_checkbox.pack(side=tk.LEFT, padx=(10,0), anchor=tk.E) # Menu menubar = tk.Menu(root, tearoff=0, background=BG_COLOR, foreground=FG_COLOR, activebackground=BUTTON_BG, activeforeground=FG_COLOR) filemenu = tk.Menu(menubar, tearoff=0, background=BG_COLOR, foreground=FG_COLOR, activebackground=BUTTON_BG, activeforeground=FG_COLOR) filemenu.add_command(label="Sauvegarder Script...", command=self.save_script, accelerator="Ctrl+S") filemenu.add_command(label="Charger Script...", command=self.load_script, accelerator="Ctrl+O") filemenu.add_separator(background=BG_COLOR) filemenu.add_command(label="Quitter", command=root.quit, accelerator="Ctrl+Q") menubar.add_cascade(label="Fichier", menu=filemenu) root.config(menu=menubar) # Raccourcis pour sauvegarder/charger (optionnel mais pratique) root.bind_all("", lambda event: self.save_script()) root.bind_all("", lambda event: self.load_script()) root.bind_all("", lambda event: root.quit()) self.update_status("Prêt. (F8: Rec/Stop, F12: Play, F10: Stop Play)") self.setup_global_hotkeys() self.toggle_loop_entry_state() # Initialize state def toggle_loop_entry_state(self): if self.loop_indefinitely_var.get(): self.loop_entry.config(state=tk.DISABLED) else: self.loop_entry.config(state=tk.NORMAL) def update_status(self, message): self.status_label.config(text=message) self.root.update_idletasks() def _log_action(self, action_type, **kwargs): if not self.is_recording: return current_time = time.perf_counter() delay = current_time - self.last_event_time if self.last_event_time else 0 self.last_event_time = current_time action = {"delay": delay, "type": action_type, **kwargs} self.recorded_actions.append(action) def on_move(self, x, y): self._log_action("mouse_move", x=x, y=y) def on_click(self, x, y, button, pressed): action_name = "mouse_press" if pressed else "mouse_release" # Stocker button.name (ex: 'left') au lieu de str(button) self._log_action(action_name, x=x, y=y, button=button.name) def on_scroll(self, x, y, dx, dy): self._log_action("mouse_scroll", x=x, y=y, dx=dx, dy=dy) def on_key_press(self, key): if key in [HOTKEY_RECORD_TOGGLE, HOTKEY_PLAY, HOTKEY_STOP_PLAYBACK]: return try: self._log_action("key_press", key=key.char) except AttributeError: self._log_action("key_press", key=str(key)) def on_key_release(self, key): if key in [HOTKEY_RECORD_TOGGLE, HOTKEY_PLAY, HOTKEY_STOP_PLAYBACK]: return try: self._log_action("key_release", key=key.char) except AttributeError: self._log_action("key_release", key=str(key)) def start_recording(self): if self.is_playing: messagebox.showwarning("Avertissement", "Veuillez arrêter la lecture avant d'enregistrer.", parent=self.root) return self.is_recording = True self.recorded_actions = [] self.last_event_time = time.perf_counter() self.update_status("🔴 Enregistrement en cours... (F8 pour arrêter)") self.record_button.config(text="⏹️ Arrêter (F8)") self.play_button.config(state=tk.DISABLED) self.loop_indefinitely_checkbox.config(state=tk.DISABLED) self.loop_entry.config(state=tk.DISABLED) self.mouse_listener = mouse.Listener(on_move=self.on_move, on_click=self.on_click, on_scroll=self.on_scroll) self.keyboard_listener = keyboard.Listener(on_press=self.on_key_press, on_release=self.on_key_release, suppress=False) self.mouse_listener.start() self.keyboard_listener.start() def stop_recording(self): if not self.is_recording: return self.is_recording = False if self.mouse_listener: self.mouse_listener.stop() self.mouse_listener = None if self.keyboard_listener: self.keyboard_listener.stop() self.keyboard_listener = None self.update_status(f"Prêt. {len(self.recorded_actions)} actions enregistrées.") self.record_button.config(text="🔴 Enregistrer (F8)") self.play_button.config(state=tk.NORMAL) self.loop_indefinitely_checkbox.config(state=tk.NORMAL) self.toggle_loop_entry_state() # Réactiver loop_entry si nécessaire def toggle_record(self): if self.is_recording: self.stop_recording() else: self.start_recording() def _play_actions(self): self.is_playing = True self.stop_playback_flag = False self.update_status("▶️ Lecture en cours... (F10 pour arrêter)") self.play_button.config(state=tk.DISABLED) self.record_button.config(state=tk.DISABLED) self.loop_indefinitely_checkbox.config(state=tk.DISABLED) self.loop_entry.config(state=tk.DISABLED) mouse_controller = mouse.Controller() keyboard_controller = keyboard.Controller() loops_str = "∞" if self.loop_indefinitely_var.get(): loops = float('inf') else: try: loops = int(self.loop_var.get()) if loops <= 0: loops = 1 loops_str = str(loops) except ValueError: loops = 1 loops_str = "1" self.loop_var.set(str(loops)) initial_playback_start_time = time.perf_counter() total_actions_played = 0 current_loop_count = 0 while current_loop_count < loops: if self.stop_playback_flag: self.update_status("⏹️ Lecture arrêtée par l'utilisateur.") break current_loop_count += 1 loop_display = "∞" if loops == float('inf') else str(int(loops)) self.update_status(f"▶️ Lecture... (Boucle {current_loop_count}/{loop_display}, F10 pour arrêter)") for action_index, action in enumerate(self.recorded_actions): if self.stop_playback_flag: break # Attendre le délai AVANT d'exécuter l'action, sauf pour la toute première action de la toute première boucle if not (current_loop_count == 1 and action_index == 0 and action["delay"] == 0): time.sleep(action["delay"]) if self.stop_playback_flag: break action_type = action["type"] if action_type == "mouse_move": mouse_controller.position = (action["x"], action["y"]) elif action_type == "mouse_press": button = MOUSE_BUTTON_MAP.get(action["button"]) if button: mouse_controller.press(button) elif action_type == "mouse_release": button = MOUSE_BUTTON_MAP.get(action["button"]) if button: mouse_controller.release(button) elif action_type == "mouse_scroll": mouse_controller.scroll(action["dx"], action["dy"]) elif action_type == "key_press": key_to_press = self.parse_key(action["key"], keyboard_controller) if key_to_press: keyboard_controller.press(key_to_press) elif action_type == "key_release": key_to_release = self.parse_key(action["key"], keyboard_controller) if key_to_release: keyboard_controller.release(key_to_release) total_actions_played +=1 if self.stop_playback_flag: break if not self.stop_playback_flag: total_time = time.perf_counter() - initial_playback_start_time self.update_status(f"🏁 Lecture terminée. ({total_actions_played} actions en {total_time:.2f}s)") self.is_playing = False self.play_button.config(state=tk.NORMAL) self.record_button.config(state=tk.NORMAL) self.loop_indefinitely_checkbox.config(state=tk.NORMAL) self.toggle_loop_entry_state() def parse_key(self, key_str, controller): if key_str.startswith("Key."): return getattr(keyboard.Key, key_str.split('.')[-1], None) elif isinstance(key_str, str) and len(key_str) == 1: return keyboard.KeyCode.from_char(key_str) # Gérer les cas où key_str pourrait être None ou une chaîne spéciale non gérée # Par exemple, pour les touches comme '' ou des combinaisons enregistrées différemment. # Pour l'instant, pynput enregistre ctrl, alt, etc. comme des Key.ctrl_l, Key.alt_l, etc. # ce qui est couvert par le premier if. # print(f"Warning: Could not parse key: {key_str}") return None def start_playback(self): if self.is_recording: messagebox.showwarning("Avertissement", "Arrêtez l'enregistrement avant la lecture.", parent=self.root) return if not self.recorded_actions: messagebox.showinfo("Info", "Aucune action enregistrée à lire.", parent=self.root) return if self.is_playing: return self.playback_thread = threading.Thread(target=self._play_actions, daemon=True) self.playback_thread.start() def _stop_playback_action(self): if self.is_playing: self.stop_playback_flag = True def setup_global_hotkeys(self): def on_activate_record(): self.root.after(0, self.toggle_record) def on_activate_play(): self.root.after(0, self.start_playback) def on_activate_stop_playback(): self.root.after(0, self._stop_playback_action) def listen_hotkeys(): hotkeys_map = { HOTKEY_RECORD_TOGGLE: on_activate_record, HOTKEY_PLAY: on_activate_play, HOTKEY_STOP_PLAYBACK: on_activate_stop_playback, } def on_press(key): if self.is_recording and key in [HOTKEY_PLAY, HOTKEY_STOP_PLAYBACK]: # Prevent play/stop during recording return if self.is_playing and key == HOTKEY_RECORD_TOGGLE: # Prevent record during play return if key in hotkeys_map: hotkeys_map[key]() with keyboard.Listener(on_press=on_press) as listener: listener.join() self.hotkey_listener_thread = threading.Thread(target=listen_hotkeys, daemon=True) self.hotkey_listener_thread.start() def save_script(self): if not self.recorded_actions: messagebox.showinfo("Info", "Rien à sauvegarder.", parent=self.root) return filepath = filedialog.asksaveasfilename( parent=self.root, title="Sauvegarder le script AutoTask", defaultextension=".json", filetypes=[("Scripts AutoTask", "*.json"), ("Tous les fichiers", "*.*")] ) if filepath: try: with open(filepath, 'w') as f: json.dump(self.recorded_actions, f, indent=4) self.update_status(f"Script sauvegardé : {filepath.split('/')[-1]}") except Exception as e: messagebox.showerror("Erreur de Sauvegarde", f"Impossible de sauvegarder : {e}", parent=self.root) def load_script(self): if self.is_recording or self.is_playing: messagebox.showwarning("Avertissement", "Arrêtez l'activité en cours avant de charger.", parent=self.root) return filepath = filedialog.askopenfilename( parent=self.root, title="Charger un script AutoTask", defaultextension=".json", filetypes=[("Scripts AutoTask", "*.json"), ("Tous les fichiers", "*.*")] ) if filepath: try: with open(filepath, 'r') as f: self.recorded_actions = json.load(f) self.update_status(f"Script chargé : {len(self.recorded_actions)} actions.") self.loop_var.set("1") # Reset loop count self.loop_indefinitely_var.set(False) # Reset infinite loop self.toggle_loop_entry_state() except Exception as e: messagebox.showerror("Erreur de Chargement", f"Impossible de charger : {e}", parent=self.root) self.recorded_actions = [] if __name__ == "__main__": root = tk.Tk() app = AutoTask(root) root.mainloop()